You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

469 lines
13 KiB

import {
render,
screen,
fireEvent,
waitFor,
cleanup,
} from "@testing-library/react";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import QuestionDetailClient from "./question-detail-client";
import { useCattellQuestionsQuery } from "@/hooks/marriage/use-cattell";
import { useGlasserQuestionsQuery } from "@/hooks/marriage/use-glasser";
import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
import {
useFormOverviewQuery,
useFormSectionQuery,
} from "@/hooks/marriage/use-form-schema";
import {
convertOverviewToFrontendItems,
mapBackendSectionToFrontend,
} from "@/lib/schema-adapter";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
vi.mock("next/navigation", () => ({
useRouter: vi.fn(() => ({ replace: vi.fn(), push: vi.fn() })),
}));
vi.mock("@/translations/provider", () => ({
useI18n: vi.fn(() => ({
locale: "en",
dictionary: new Proxy({}, { get: (_, key) => key }),
})),
}));
vi.mock("@/hooks/marriage/use-cattell", () => ({
useCattellQuestionsQuery: vi.fn(),
useSubmitCattellAssessmentMutation: vi.fn(() => ({ mutateAsync: vi.fn() })),
}));
vi.mock("@/hooks/marriage/use-glasser", () => ({
useGlasserQuestionsQuery: vi.fn(),
useSubmitGlasserAssessmentMutation: vi.fn(() => ({ mutateAsync: vi.fn() })),
}));
vi.mock("@/hooks/marriage/use-profile-main", () => ({
useMarriageProfileQuery: vi.fn(),
}));
vi.mock("@/hooks/marriage/use-form-schema", () => ({
useFormOverviewQuery: vi.fn(),
useFormSectionQuery: vi.fn(),
}));
vi.mock("@/hooks/marriage/use-habcoin-payment", () => ({
useHabcoinPaymentMutation: vi.fn(() => ({ mutateAsync: vi.fn() })),
}));
vi.mock("@/lib/schema-adapter", () => ({
convertOverviewToFrontendItems: vi.fn(),
mapBackendSectionToFrontend: vi.fn(),
}));
describe("QuestionDetailClient Validation", () => {
const mockCattellRefetch = vi.fn();
const mockGlasserRefetch = vi.fn();
afterEach(() => {
cleanup();
});
beforeEach(() => {
vi.clearAllMocks();
(useMarriageProfileQuery as any).mockReturnValue({
data: { age: 30, gender: "male" },
isLoading: false,
});
(useFormOverviewQuery as any).mockReturnValue({
data: {},
isLoading: false,
});
(useFormSectionQuery as any).mockReturnValue({
data: undefined,
isLoading: false,
});
});
const setupTest = (slug: string, cattellData: any, glasserData: any) => {
// Mock schema adapter to return an item for the requested slug
(convertOverviewToFrontendItems as any).mockReturnValue([
{
slug: slug,
title: "Test",
questions: [],
},
]);
(useCattellQuestionsQuery as any).mockReturnValue({
data: cattellData,
isLoading: false,
isError: false,
refetch: mockCattellRefetch,
});
(useGlasserQuestionsQuery as any).mockReturnValue({
data: glasserData,
isLoading: false,
isError: false,
refetch: mockGlasserRefetch,
});
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
render(
<QueryClientProvider client={queryClient}>
<QuestionDetailClient
closeLabel="Close"
continueLabel="Continue"
description="Desc"
informationLabel="Info"
itemSlug={slug}
questionsListHref="/questions-list"
title="Test"
/>
</QueryClientProvider>,
);
// Intro screen might render first; if Start is available, click it to mount questions flow
const startButton = screen.queryByText("Start");
if (startButton) {
fireEvent.click(startButton);
}
};
it("should render correctly when Cattell API data is completely valid", () => {
setupTest(
"personality_test",
{
questions: [
{
question_number: 1,
text: "Valid Question Cattell",
options: [
{ id: "opt_a", label: "Opt1", value: "A" },
{ id: "opt_b", label: "Opt2", value: "B" },
{ id: "opt_c", label: "Opt3", value: "C" },
],
},
],
},
null,
);
// Retry UI should NOT be present
expect(screen.queryByText("Retry")).toBeNull();
// Question text should be visible
expect(screen.getByText("Valid Question Cattell")).toBeDefined();
});
it("should render Retry UI when Cattell API data is empty", () => {
setupTest("personality_test", { questions: [] }, null);
expect(
screen.getAllByText("No questions found for this test."),
).toBeDefined();
expect(screen.getAllByText("Retry")).toBeDefined();
});
it("should render Retry UI when Cattell options are invalid (schema failure) and trigger refetch on Retry", async () => {
setupTest(
"personality_test",
{
questions: [
{
question_number: 1,
text: "Invalid Question",
options: [{ label: "Opt1", value: "A" }], // Invalid schema
},
],
},
null,
);
expect(
screen.getAllByText("No questions found for this test."),
).toBeDefined();
const retryBtn = screen.getAllByText("Retry")[0];
fireEvent.click(retryBtn);
await waitFor(() => {
expect(mockCattellRefetch).toHaveBeenCalled();
});
});
it("should render correctly when Glasser API data is completely valid", () => {
setupTest("glasser_5_needs_test", null, {
questions: [
{
question_number: 1,
text: "Valid Question Glasser",
factor_code: "SUR",
options: [
{ id: "o1", label: "O1", value: 1 },
{ id: "o2", label: "O2", value: 2 },
{ id: "o3", label: "O3", value: 3 },
{ id: "o4", label: "O4", value: 4 },
{ id: "o5", label: "O5", value: 5 },
],
},
],
});
expect(screen.queryByText("Retry")).toBeNull();
expect(screen.getByText("Valid Question Glasser")).toBeDefined();
});
it("should render Retry UI when Glasser options are invalid (schema failure) and trigger refetch on Retry", async () => {
setupTest("glasser_5_needs_test", null, {
questions: [
{
question_number: 1,
text: "Invalid Question Glasser",
factor_code: "SUR",
options: [
{ label: "O1", value: 1 },
{ label: "O2", value: 2 },
{ label: "O3", value: 3 },
{ label: "O4", value: 4 },
],
},
],
});
expect(
screen.getAllByText("No questions found for this test."),
).toBeDefined();
const retryBtn = screen.getAllByText("Retry")[0];
fireEvent.click(retryBtn);
await waitFor(() => {
expect(mockGlasserRefetch).toHaveBeenCalled();
});
});
it("should render profile questions using ID-based data flow", () => {
const profileItem = {
slug: "profile_test",
title: "Profile Form",
questions: [
{
id: "q_123",
title: "Dynamic ID Question",
type: "text",
order: 1,
required: true,
isVisible: true,
private: false,
description: "",
tooltip: "",
extras: {},
options: [],
},
{
id: "q_456",
title: "Another ID Question",
type: "radio",
order: 2,
required: false,
isVisible: true,
private: false,
description: "",
tooltip: "",
extras: {},
options: [
{ id: "opt_1", label: "Yes", value: "yes", order: 1 },
{ id: "opt_2", label: "No", value: "no", order: 2 },
],
},
],
};
(convertOverviewToFrontendItems as any).mockReturnValue([
{
slug: "profile_test",
title: "Profile Form",
questions: [],
},
]);
(useFormSectionQuery as any).mockReturnValue({
data: {
section: { cards: [] },
answers: {},
section_progress: { completion_percent: 0 },
},
isLoading: false,
});
(mapBackendSectionToFrontend as any).mockReturnValue(profileItem);
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
render(
<QueryClientProvider client={queryClient}>
<QuestionDetailClient
closeLabel="Close"
continueLabel="Continue"
description="Desc"
informationLabel="Info"
itemSlug="profile_test"
questionsListHref="/questions-list"
title="Profile Test"
/>
</QueryClientProvider>,
);
// Profile questions render directly, no start button
expect(screen.getByText(/Dynamic ID/)).toBeDefined();
});
it("renders a stable profile loading shell while section data is cold", () => {
(convertOverviewToFrontendItems as any).mockReturnValue([
{
slug: "profile_test",
title: "Overview Profile Title",
questions: [],
},
]);
(useFormSectionQuery as any).mockReturnValue({
data: undefined,
isLoading: true,
});
(useCattellQuestionsQuery as any).mockReturnValue({
data: undefined,
isLoading: false,
});
(useGlasserQuestionsQuery as any).mockReturnValue({
data: undefined,
isLoading: false,
});
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
render(
<QueryClientProvider client={queryClient}>
<QuestionDetailClient
closeLabel="Close"
continueLabel="Continue"
description="Desc"
informationLabel="Info"
itemSlug="profile_test"
questionsListHref="/questions-list"
title="Route Fallback Title"
/>
</QueryClientProvider>,
);
expect(screen.getByText("Overview Profile Title")).toBeDefined();
expect(screen.getByRole("status")).toBeDefined();
expect(screen.getByRole("button", { name: "Close" })).toBeDefined();
expect(screen.getByRole("button", { name: "Info" })).toBeDefined();
expect(screen.getByRole("button", { name: "Continue" })).toBeDisabled();
expect(document.querySelector(".shimmer-bg")).toBeNull();
});
it("renders cached section questions while the overview refreshes", () => {
const cachedItem = {
slug: "profile_test",
title: "Cached Profile Form",
questions: [
{
id: "cached_question",
title: "Cached question",
type: "text",
order: 1,
required: true,
isVisible: true,
private: false,
description: "",
tooltip: "",
extras: { placeHolder: "", range: [0, 0], options: [] },
options: [],
},
],
};
(convertOverviewToFrontendItems as any).mockReturnValue([]);
(useFormOverviewQuery as any).mockReturnValue({
data: undefined,
isLoading: true,
});
(useFormSectionQuery as any).mockReturnValue({
data: {
section: { cards: [] },
answers: {},
section_progress: { completion_percent: 0 },
},
isLoading: false,
});
(mapBackendSectionToFrontend as any).mockReturnValue(cachedItem);
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
render(
<QueryClientProvider client={queryClient}>
<QuestionDetailClient
closeLabel="Close"
continueLabel="Continue"
description="Desc"
informationLabel="Info"
itemSlug="profile_test"
questionsListHref="/questions-list"
title="Route Fallback Title"
/>
</QueryClientProvider>,
);
expect(screen.getByRole("textbox")).toBeDefined();
expect(screen.queryByRole("status")).toBeNull();
});
it("uses the route title when the overview is also cold", () => {
(convertOverviewToFrontendItems as any).mockReturnValue([]);
(useFormOverviewQuery as any).mockReturnValue({
data: undefined,
isLoading: true,
});
(useFormSectionQuery as any).mockReturnValue({
data: undefined,
isLoading: true,
});
(useCattellQuestionsQuery as any).mockReturnValue({
data: undefined,
isLoading: false,
});
(useGlasserQuestionsQuery as any).mockReturnValue({
data: undefined,
isLoading: false,
});
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
render(
<QueryClientProvider client={queryClient}>
<QuestionDetailClient
closeLabel="Close"
continueLabel="Continue"
description="Desc"
informationLabel="Info"
itemSlug="profile_test"
questionsListHref="/questions-list"
title="Route Fallback Title"
/>
</QueryClientProvider>,
);
expect(screen.getByText("Route Fallback Title")).toBeDefined();
expect(screen.getByRole("status")).toBeDefined();
expect(document.querySelector(".shimmer-bg")).toBeNull();
});
});